
{{ template "page/head_start" .}}
<style>
  .ant-table:not(.ant-table-expanded-row .ant-table) {
    outline: 1px solid #f0f0f0;
    outline-offset: -1px;
    border-radius: 1rem;
    overflow-x: hidden;
  }
  // 【新增】: 确保表格标题不换行 
  .ant-table-thead > tr > th {
      white-space: nowrap;
  }
  .dark .ant-table:not(.ant-table-expanded-row .ant-table) {
    outline-color: var(--dark-color-table-ring);
  }
	
.ant-modal-body .ant-input-group.ant-input-group-compact {
  display: block;
}

.ant-modal-body .ant-input-group.ant-input-group-compact .ant-input-affix-wrapper,
.ant-modal-body .ant-input-group.ant-input-group-compact .ant-input-search-button {
  width: 100%;
}

.ant-modal-body .ant-input-group.ant-input-group-compact .ant-input {
  white-space: normal;
  height: auto;
  min-height: 80px; /* 调整此值以控制显示行数 */
}	
  .ant-table .ant-table-content .ant-table-scroll .ant-table-body {
    overflow-y: hidden;
  }
  .ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
	  margin:-10px 22px !important;
  }
  .ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper .ant-table {
    border-bottom-left-radius: 1rem;
    border-bottom-right-radius: 1rem;
  }
  .ant-table .ant-table-content .ant-table-tbody tr:last-child tr:last-child td {
	  border-bottom-color: transparent;
  }
  .ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:first-child {
    border-bottom-left-radius: 6px;
  }
  .ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:last-child {
    border-bottom-right-radius: 6px;
  }
  @media (min-width: 769px) {
    .ant-layout-content {
      margin: 24px 16px;
    }
  }
  @media (max-width: 768px) {
    .ant-card-body {
      padding: .5rem;
    }
    .ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
	    margin:-10px 2px !important;
    }
  }
  .dark .ant-switch-small:not(.ant-switch-checked) {
    background-color: var(--dark-color-surface-100) !important;
  }
  .ant-custom-popover-title {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 5px 0;
  }
  .ant-col-sm-24 {
    margin: 0.5rem -2rem 0.5rem 2rem;
  }
  tr.hideExpandIcon .ant-table-row-expand-icon {
    display: none;
  }
  .infinite-tag {
    padding: 0 5px;
    border-radius: 2rem;
    min-width: 50px;
    min-height: 22px;
  }
  .infinite-bar .ant-progress-inner .ant-progress-bg {
    background-color: #F2EAF1;
    border: #D5BED2 solid 1px;
  }
  .dark .infinite-bar .ant-progress-inner .ant-progress-bg {
    background-color: #7a316f !important;
    border: #7a316f solid 1px;
  }
  .ant-collapse {
    margin: 5px 0;
  }
  .info-large-tag {
    max-width: 200px;
    overflow: hidden;
  }
  .client-comment {
    font-size: 12px;
    opacity: 0.75;
    cursor: help;
  }
  .client-email {
    font-weight: 500;
  }
  .client-popup-item {
    display: flex;
    align-items: center;
    gap: 5px;
  }
  .online-animation .ant-badge-status-dot {
    animation: onlineAnimation 1.2s linear infinite;
  }
  @keyframes onlineAnimation {
    0%,
    50%,
    100% {
      transform: scale(1);
      opacity: 1;
    }
    10% {
      transform: scale(1.5);
      opacity: .2;
    }
  }
  .tr-table-box {
    display: flex;
    gap: 4px;
    justify-content: center;
    align-items: center;
  }
  .tr-table-rt {
    flex-basis: 70px;
    min-width: 70px;
    text-align: end;
  }
  .tr-table-lt {
    flex-basis: 70px;
    min-width: 70px;
    text-align: start;
  }
  .tr-table-bar {
    flex-basis: 160px;
    min-width: 60px;
  }
  .tr-infinity-ch {
    font-size: 14pt;
    max-height: 24px;
    display: inline-flex;
    align-items: center;
  }
  .ant-table-expanded-row .ant-table .ant-table-body {
    overflow-x: hidden;
  }
  .ant-table-expanded-row .ant-table-tbody>tr>td {
    padding: 10px 2px;
  }
  .ant-table-expanded-row .ant-table-thead>tr>th {
    padding: 12px 2px;
  }
</style>
{{ template "page/head_end" .}}

{{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
  <a-sidebar></a-sidebar>
  <a-layout id="content-layout">
    <a-layout-content>
      <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
        <transition name="list" appear>
          <a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
            message='{{ i18n "secAlertTitle" }}'
            color="red"
            description='{{ i18n "secAlertSsl" }}'
            show-icon closable>
          </a-alert>
        </transition>
        <transition name="list" appear>
          <a-row v-if="!loadingStates.fetched">
            <a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
              <a-spin tip='{{ i18n "loading" }}'></a-spin>
            </a-card>
          </a-row>
          <a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
            <a-col>
              <a-card size="small" :style="{ padding: '16px' }" hoverable>
                <a-row>
                  <a-col :sm="12" :md="5">
                    <a-custom-statistic title='{{ i18n "pages.inbounds.totalDownUp" }}' :value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`">
                      <template #prefix>
                        <a-icon type="swap"></a-icon>
                      </template>
                    </a-custom-statistic>
                  </a-col>
                  <a-col :sm="12" :md="5">
                    <a-custom-statistic title='{{ i18n "pages.inbounds.totalUsage" }}' :value="SizeFormatter.sizeFormat(total.up + total.down)" :style="{ marginTop: isMobile ? '10px' : 0 }">
                      <template #prefix>
                        <a-icon type="pie-chart"></a-icon>
                      </template>
                    </a-custom-statistic>
                  </a-col>
                  <a-col :sm="12" :md="5">
                    <a-custom-statistic title='{{ i18n "pages.inbounds.allTimeTrafficUsage" }}' :value="SizeFormatter.sizeFormat(total.allTime)" :style="{ marginTop: isMobile ? '10px' : 0 }">
                      <template #prefix>
                        <a-icon type="history"></a-icon>
                      </template>
                    </a-custom-statistic>
                  </a-col>
                  <a-col :sm="12" :md="5">
                    <a-custom-statistic title='{{ i18n "pages.inbounds.inboundCount" }}' :value="dbInbounds.length" :style="{ marginTop: isMobile ? '10px' : 0 }">
                      <template #prefix>
                        <a-icon type="bars"></a-icon>
                      </template>
                    </a-custom-statistic>
                  </a-col>
                  <a-col :sm="12" :md="4">
                    <a-custom-statistic title='{{ i18n "clients" }}' value=" " :style="{ marginTop: isMobile ? '10px' : 0 }">
                      <template #prefix>
                        <a-space direction="horizontal">
                          <a-icon type="team"></a-icon>
                          <div>
                            <a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
                            <a-tag color="green">[[ total.clients ]]</a-tag>
                            <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
                              <template slot="content">
                                <div v-for="clientEmail in total.deactive"><span>[[ clientEmail ]]</span></div>
                              </template>
                              <a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
                            </a-popover>
                            <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
                              <template slot="content">
                                <div v-for="clientEmail in total.depleted"><span>[[ clientEmail ]]</span></div>
                              </template>
                              <a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
                            </a-popover>
                            <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
                              <template slot="content">
                                <div v-for="clientEmail in total.expiring"><span>[[ clientEmail ]]</span></div>
                              </template>
                              <a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag>
                            </a-popover>
                            <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
                              <template slot="content">
                                <div v-for="clientEmail in onlineClients"><span>[[ clientEmail ]]</span></div>
                              </template>
                              <a-tag color="blue" v-if="onlineClients.length">[[ onlineClients.length ]]</a-tag>
                            </a-popover>
                          </div>
                        </a-space>
                      </template>
                    </a-custom-statistic>
                  </a-col>
                </a-row>
              </a-card>
            </a-col>
            <a-col>
              <a-card hoverable>
                <template #title>
                  <a-space direction="horizontal">
                    <a-button type="primary" icon="plus" @click="openAddInbound">
                      <template v-if="!isMobile">{{ i18n "pages.inbounds.addInbound" }}</template>
                    </a-button>
                    <a-dropdown :trigger="['click']">
                      <a-button type="primary" icon="menu">
                        <template v-if="!isMobile">{{ i18n "pages.inbounds.generalActions" }}</template>
                      </a-button>
                      <a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
                        <a-menu-item key="import">
                          <a-icon type="import"></a-icon>
                          {{ i18n "pages.inbounds.importInbound" }}
                        </a-menu-item>
                        <a-menu-item key="export">
                          <a-icon type="export"></a-icon>
                          {{ i18n "pages.inbounds.export" }}
                        </a-menu-item>
                        <a-menu-item key="subs" v-if="subSettings.enable">
                          <a-icon type="export"></a-icon>
                          {{ i18n "pages.inbounds.export" }} - {{ i18n "pages.settings.subSettings" }}
                        </a-menu-item>
                        <a-menu-item key="resetInbounds">
                          <a-icon type="reload"></a-icon>
                          {{ i18n "pages.inbounds.resetAllTraffic" }}
                        </a-menu-item>
                        <a-menu-item key="resetClients">
                          <a-icon type="file-done"></a-icon>
                          {{ i18n "pages.inbounds.resetAllClientTraffics" }}
                        </a-menu-item>
                        <a-menu-item key="delDepletedClients" :style="{ color: '#FF4D4F' }">
                          <a-icon type="rest"></a-icon>
                          {{ i18n "pages.inbounds.delDepletedClients" }}
                        </a-menu-item>
                      </a-menu>
                    </a-dropdown>
                    <a-button type="primary" icon="thunderbolt" @click="openOneClickConfig">
                      <template v-if="!isMobile">{{ i18n "pages.inbounds.oneClickConfig" }}</template>
                    </a-button>
                    <a-button type="primary" icon="link" @click="handleSubscriptionConversion">
                      <template v-if="!isMobile">{{ i18n "pages.inbounds.is_subConversion" }}</template>
                    </a-button>
                  </a-space>
                </template>
                <template #extra>
                  <a-button-group>
                    <a-button icon="sync" @click="manualRefresh" :loading="refreshing"></a-button>
                    <a-popover placement="bottomRight" trigger="click" :overlay-class-name="themeSwitcher.currentTheme">
                      <template #title>
                        <div class="ant-custom-popover-title">
                          <a-switch v-model="isRefreshEnabled" @change="toggleRefresh" size="small"></a-switch>
                          <span>{{ i18n "pages.inbounds.autoRefresh" }}</span>
                        </div>
                      </template>
                      <template #content>
                        <a-space direction="vertical">
                          <span>{{ i18n "pages.inbounds.autoRefreshInterval" }}</span>
                          <a-select v-model="refreshInterval"
                              :disabled="!isRefreshEnabled"
                              :style="{ width: '100%' }"
                              @change="changeRefreshInterval"
                              :dropdown-class-name="themeSwitcher.currentTheme">
                            <a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
                          </a-select>
                        </a-space>
                      </template>
                      <a-button icon="down"></a-button>
                    </a-popover>
                  </a-button-group>
                </template>
                <a-space direction="vertical">
                  <div :style="isMobile ? {} : { display: 'flex', alignItems: 'center', justifyContent: 'flex-start' }">
                    <a-switch v-model="enableFilter"
                        :style="isMobile ? { marginBottom: '.5rem', display: 'flex' } : { marginRight: '.5rem' }"
                        @change="toggleFilter">
                      <a-icon slot="checkedChildren" type="search"></a-icon>
                      <a-icon slot="unCheckedChildren" type="filter"></a-icon>
                    </a-switch>
                    <a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus :style="{ maxWidth: '300px' }" :size="isMobile ? 'small' : ''"></a-input>
                    <a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid" :size="isMobile ? 'small' : ''">
                      <a-radio-button value="">{{ i18n "none" }}</a-radio-button>
                      <a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
                      <a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
                      <a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
                      <a-radio-button value="online">{{ i18n "online" }}</a-radio-button>
                    </a-radio-group>
                  </div>
                  <a-table :columns="isMobile ? mobileColumns : columns" :row-key="dbInbound => dbInbound.id"
                      :data-source="searchedInbounds"
                      :scroll="isMobile ? {} : { x: 1000 }"
                      :pagination=pagination(searchedInbounds)
                      :expand-icon-as-cell="false"
                      :expand-row-by-click="false"
                      :expand-icon-column-index="0"
                      :indent-size="0"
                      :row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')"
                      :style="{ marginTop: '10px' }"
                      :locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}`, emptyText: `{{ i18n "noData" }}` }'>
                    <template slot="action" slot-scope="text, dbInbound">
                      <a-dropdown :trigger="['click']">
                        <a-icon @click="e => e.preventDefault()" type="more" :style="{ fontSize: '20px', textDecoration: 'solid' }"></a-icon>
                        <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme">
                          <a-menu-item key="edit">
                            <a-icon type="edit"></a-icon>
                            {{ i18n "edit" }}
                          </a-menu-item>
                          <a-menu-item key="qrcode" v-if="(dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser) || dbInbound.isWireguard">
                            <a-icon type="qrcode"></a-icon>
                            {{ i18n "qrCode" }}
                          </a-menu-item>
                          <template v-if="dbInbound.isMultiUser()">
                            <a-menu-item key="addClient">
                              <a-icon type="user-add"></a-icon>
                              {{ i18n "pages.client.add"}}
                            </a-menu-item>
                            <a-menu-item key="addBulkClient">
                              <a-icon type="usergroup-add"></a-icon>
                              {{ i18n "pages.client.bulk"}}
                            </a-menu-item>
                            <a-menu-item key="resetClients">
                              <a-icon type="file-done"></a-icon>
                              {{ i18n "pages.inbounds.resetInboundClientTraffics"}}
                            </a-menu-item>
                            <a-menu-item key="export">
                              <a-icon type="export"></a-icon>
                              {{ i18n "pages.inbounds.export"}}
                            </a-menu-item>
                            <a-menu-item key="subs" v-if="subSettings.enable">
                              <a-icon type="export"></a-icon>
                              {{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}
                            </a-menu-item>
                            <a-menu-item key="delDepletedClients" :style="{ color: '#FF4D4F' }">
                              <a-icon type="rest"></a-icon>
                              {{ i18n "pages.inbounds.delDepletedClients" }}
                            </a-menu-item>
                          </template>
                          <template v-else>
                            <a-menu-item key="showInfo">
                              <a-icon type="info-circle"></a-icon>
                              {{ i18n "info"}}
                            </a-menu-item>
                          </template>
                          <a-menu-item key="clipboard">
                            <a-icon type="copy"></a-icon>
                            {{ i18n "pages.inbounds.exportInbound" }}
                          </a-menu-item>
                          <a-menu-item key="resetTraffic">
                            <a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
                          </a-menu-item>
                          <a-menu-item key="clone">
                            <a-icon type="block"></a-icon> {{ i18n "pages.inbounds.clone"}}
                          </a-menu-item>
                          <a-menu-item key="delete">
                            <span :style="{ color: '#FF4D4F' }">
                              <a-icon type="delete"></a-icon> {{ i18n "delete"}}
                            </span>
                          </a-menu-item>
                          <a-menu-item v-if="isMobile">
                            <a-switch size="small" v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
                            {{ i18n "pages.inbounds.enable" }}
                          </a-menu-item>
                        </a-menu>
                      </a-dropdown>
                    </template>
                    <template slot="protocol" slot-scope="text, dbInbound">
                      <a-tag :style="{ margin: '0' }" color="purple">[[ dbInbound.protocol ]]</a-tag>
                      <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
                        <a-tag :style="{ margin: '0' }" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
                        <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isTls" color="blue">TLS</a-tag>
                        <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isReality" color="blue">Reality</a-tag>
                      </template>
                    </template>
                    <template slot="clients" slot-scope="text, dbInbound">
                      <template v-if="clientCount[dbInbound.id]">
                        <a-tag :style="{ margin: '0' }" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
                        <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
                          <template slot="content">
                            <div v-for="clientEmail in clientCount[dbInbound.id].deactive" :key="clientEmail" class="client-popup-item">
                              <span>[[ clientEmail ]]</span>
                              <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                <template #title>
                                  [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                </template>
                                <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                              </a-tooltip>
                            </div>
                          </template>
                          <a-tag :style="{ margin: '0', padding: '0 2px' }" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
                        </a-popover>
                        <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
                          <template slot="content">
                            <div v-for="clientEmail in clientCount[dbInbound.id].depleted" :key="clientEmail" class="client-popup-item">
                              <span>[[ clientEmail ]]</span>
                              <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                <template #title>
                                  [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                </template>
                                <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                              </a-tooltip>
                            </div>
                          </template>
                          <a-tag :style="{ margin: '0', padding: '0 2px' }" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
                        </a-popover>
                        <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
                          <template slot="content">
                            <div v-for="clientEmail in clientCount[dbInbound.id].expiring" :key="clientEmail" class="client-popup-item">
                              <span>[[ clientEmail ]]</span>
                              <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                <template #title>
                                  [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                </template>
                                <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                              </a-tooltip>
                            </div>
                          </template>
                          <a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
                        </a-popover>
                        <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
                          <template slot="content">
                            <div v-for="clientEmail in clientCount[dbInbound.id].online" :key="clientEmail" class="client-popup-item">
                              <span>[[ clientEmail ]]</span>
                              <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                <template #title>
                                  [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                </template>
                                <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                              </a-tooltip>
                            </div>
                          </template>
                          <a-tag :style="{ margin: '0', padding: '0 2px' }" color="blue" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
                        </a-popover>
                      </template>
                    </template>
				    <template slot="deviceLimit" slot-scope="text, dbInbound">
                        <span v-if="dbInbound.deviceLimit > 0">[[ dbInbound.deviceLimit ]]</span>
                        <span v-else>{{ i18n "pages.inbounds.unlimited" }}</span>
                    </template>
                    <template slot="traffic" slot-scope="text, dbInbound">
                      <a-popover :overlay-class-name="themeSwitcher.currentTheme">
                        <template slot="content">
                          <table cellpadding="2" width="100%">
                            <tr>
                              <td>↑[[ SizeFormatter.sizeFormat(dbInbound.up) ]]</td>
                              <td>↓[[ SizeFormatter.sizeFormat(dbInbound.down) ]]</td>
                            </tr>
                            <tr v-if="dbInbound.total > 0 &&  dbInbound.up + dbInbound.down < dbInbound.total">
                              <td>{{ i18n "remained" }}</td>
                              <td>[[ SizeFormatter.sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
                            </tr>
                          </table>
                        </template>
                        <a-tag :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
                          [[ SizeFormatter.sizeFormat(dbInbound.up + dbInbound.down) ]] /
                          <template v-if="dbInbound.total > 0">
                              [[ SizeFormatter.sizeFormat(dbInbound.total) ]]
                          </template>
                          <template v-else>
                            <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
                              <path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
                            </svg>
                          </template>
                        </a-tag>
                      </a-popover>
                    </template>
                    <template slot="allTimeInbound" slot-scope="text, dbInbound">
                      <a-tag>[[ SizeFormatter.sizeFormat(dbInbound.allTime || 0) ]]</a-tag>
                    </template>
                    <template slot="enable" slot-scope="text, dbInbound">
                      <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
                    </template>
                    <template slot="expiryTime" slot-scope="text, dbInbound">
                      <a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
                        <template slot="content" v-if="app.datepicker === 'gregorian'">
                          [[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
                        </template>
                        <template v-else slot="content">
                          [[ DateUtil.convertToJalalian(moment(dbInbound.expiryTime)) ]]
                        </template>
                        <a-tag :style="{ minWidth: '50px' }" :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
                          [[ remainedDays(dbInbound._expiryTime) ]]
                        </a-tag>
                      </a-popover>
                      <a-tag v-else color="purple" class="infinite-tag">
                        <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
                          <path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
                        </svg>
                      </a-tag>
                    </template>
                    <template slot="info" slot-scope="text, dbInbound">
                      <a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
                        <template slot="content">
                          <table cellpadding="2">
                            <tr>
                              <td>{{ i18n "pages.inbounds.protocol" }}</td>
                              <td>
                                <a-tag :style="{ margin: '0' }" color="purple">[[ dbInbound.protocol ]]</a-tag>
                                <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
                                  <a-tag :style="{ margin: '0' }" color="blue">[[ dbInbound.toInbound().stream.network ]]</a-tag>
                                  <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isTls" color="green">tls</a-tag>
                                  <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isReality" color="green">reality</a-tag>
                                </template>
                              </td>
                            </tr>
                            <tr>
                              <td>{{ i18n "pages.inbounds.port" }}</td>
                              <td><a-tag>[[ dbInbound.port ]]</a-tag></td>
                            </tr>
                            <tr v-if="clientCount[dbInbound.id]">
                              <td>{{ i18n "clients" }}</td>
                              <td>
                                <a-tag :style="{ margin: '0' }" color="blue">[[ clientCount[dbInbound.id].clients ]]</a-tag>
                                <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
                                  <template slot="content">
                                    <div v-for="clientEmail in clientCount[dbInbound.id].deactive" :key="clientEmail" class="client-popup-item">
                                      <span>[[ clientEmail ]]</span>
                                      <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                        <template #title>
                                          [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                        </template>
                                        <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                      </a-tooltip>
                                    </div>
                                  </template>
                                  <a-tag :style="{ margin: '0', padding: '0 2px' }" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
                                </a-popover>
                                <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
                                  <template slot="content">
                                    <div v-for="clientEmail in clientCount[dbInbound.id].depleted" :key="clientEmail" class="client-popup-item">
                                      <span>[[ clientEmail ]]</span>
                                      <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                        <template #title>
                                          [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                        </template>
                                        <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                      </a-tooltip>
                                    </div>
                                  </template>
                                  <a-tag :style="{ margin: '0', padding: '0 2px' }" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
                                </a-popover>
                                <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
                                  <template slot="content">
                                    <div v-for="clientEmail in clientCount[dbInbound.id].expiring" :key="clientEmail" class="client-popup-item">
                                      <span>[[ clientEmail ]]</span>
                                      <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                        <template #title>
                                          [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                        </template>
                                        <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                      </a-tooltip>
                                    </div>
                                  </template>
                                  <a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
                                </a-popover>
                                <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
                                  <template slot="content">
                                    <div v-for="clientEmail in clientCount[dbInbound.id].online" :key="clientEmail" class="client-popup-item">
                                      <span>[[ clientEmail ]]</span>
                                      <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
                                        <template #title>
                                          [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
                                        </template>
                                        <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
                                      </a-tooltip>
                                    </div>
                                  </template>
                                  <a-tag :style="{ margin: '0', padding: '0 2px' }" color="green" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
                                </a-popover>
                              </td>
                            </tr>
                            <tr>
                              <td>{{ i18n "pages.inbounds.traffic" }}</td>
                              <td>
                                <a-popover :overlay-class-name="themeSwitcher.currentTheme">
                                  <template slot="content">
                                    <table cellpadding="2" width="100%">
                                      <tr>
                                        <td>↑[[ SizeFormatter.sizeFormat(dbInbound.up) ]]</td>
                                        <td>↓[[ SizeFormatter.sizeFormat(dbInbound.down) ]]</td>
                                      </tr>
                                      <tr v-if="dbInbound.total > 0 &&  dbInbound.up + dbInbound.down < dbInbound.total">
                                        <td>{{ i18n "remained" }}</td>
                                        <td>[[ SizeFormatter.sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
                                      </tr>
                                    </table>
                                  </template>
                                  <a-tag :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
                                      [[ SizeFormatter.sizeFormat(dbInbound.up + dbInbound.down) ]] /
                                      <template v-if="dbInbound.total > 0">
                                          [[ SizeFormatter.sizeFormat(dbInbound.total) ]]
                                      </template>
                                    <template v-else>
                                      <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
                                        <path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
                                      </svg>
                                    </template>
                                  </a-tag>
                                </a-popover>
                              </td>
                            </tr>
                            <tr>
                              <td>{{ i18n "pages.inbounds.expireDate" }}</td>
                              <td>
                                <a-tag :style="{ minWidth: '50px', textAlign: 'center' }" v-if="dbInbound.expiryTime > 0"
                                  :color="dbInbound.isExpiry? 'red': 'blue'">
                                  <template v-if="app.datepicker === 'gregorian'">
                                    [[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
                                  </template>
                                  <template v-else>
                                    [[ DateUtil.convertToJalalian(moment(dbInbound.expiryTime)) ]]
                                  </template>
                                </a-tag>
                                <a-tag v-else :style="{ textAlign: 'center' }" color="purple" class="infinite-tag">
                                  <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
                                    <path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
                                  </svg>
                                </a-tag>
                              </td>
                            </tr>
                          </table>
                        </template>
                        <a-badge>
                          <a-icon v-if="!dbInbound.enable" slot="count" type="pause-circle" :style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon>
                          <a-button shape="round" size="small" :style="{ fontSize: '14px', padding: '0 10px' }">
                            <a-icon type="info"></a-icon>
                          </a-button>
                        </a-badge>
                      </a-popover>
                    </template>
                    <template slot="expandedRowRender" slot-scope="record">
                      <a-table
                        :row-key="client => client.id"
                        :columns="isMobile ? innerMobileColumns : innerColumns"
                        :data-source="getInboundClients(record)"
                        :pagination=pagination(getInboundClients(record))
                        :style="{ margin: `-10px ${isMobile ? '2px' : '22px'} -11px` }">
                        {{template "component/aClientTable"}}
                      </a-table>
                    </template>
                  </a-table>
                </a-space>
              </a-card>
            </a-col>
          </a-row>
        </transition>
      </a-spin>
    <a-modal
      :title="`{{ i18n "pages.inbounds.oneClick.title" }}`"
      :visible="oneClickModalVisible"
      :cancel-text="`{{ i18n "cancel" }}`"
      :class="themeSwitcher.currentTheme"
      @cancel="handleOneClickCancel"
     :ok-text="null"
     :ok-button-props="{ style: { display: 'none' } }"
   >
    <a-tabs default-active-key="1">
        <a-tab-pane key="1" :tab="`{{ i18n "pages.inbounds.oneClick.generateTab" }}`">
            <div v-if="!inboundLink" style="padding: 24px;">
                    <p>{{ i18n "pages.inbounds.oneClick.prompt" }}</p>
                    
                    <a-form-item :label="`{{ i18n "pages.inbounds.oneClick.presetType" }}`">
                        <a-button @click="handleOneClickOk('vless_reality')" block style="margin-bottom: 10px;" html-type="button">
                        {{ i18n "pages.inbounds.oneClick.preset.vless_reality" }}
                        </a-button>
                        <a-button @click="handleOneClickOk('vless_tls_encryption')" block style="margin-bottom: 10px;" html-type="button">
                        {{ i18n "pages.inbounds.oneClick.preset.vless_tls_encryption" }}
                        </a-button>
                        <a-button @click="handleOneClickOk('switch_vision_seed')" disabled block>
                        {{ i18n "pages.inbounds.oneClick.preset.switch_vision_seed_dev" }}
                        </a-button>
                    </a-form-item>

                    <p><b>{{ i18n "pages.inbounds.oneClick.descriptionTitle" }}</b></p>
				    <p style="white-space: pre-wrap;">{{ i18n "pages.inbounds.oneClick.descriptionContent" }}</p>
                </div>
                
                <div v-else style="padding: 24px;">
                    <div 
                      @click="copyLink(inboundLink)" 
                      style="text-align: center; margin-bottom: 20px; cursor: pointer; padding: 10px; border-radius: 8px;"
					  :title="`{{ i18n "pages.inbounds.oneClick.clickToCopyTitle" }}`"
                    >
                      <p style="font-weight: bold; font-size: 16px; margin-bottom: 10px;">
                        {{ i18n "pages.inbounds.oneClick.scanQrcodeTitle" }}
                      </p>
                      
                      <canvas id="oneClickQrcodeCanvas"></canvas>
                      
                      <p style="margin-top: 8px; font-size: 12px; color: #888;">
                        {{ i18n "pages.inbounds.oneClick.clickToCopyHint" }}
                      </p>
                    </div>

                    <p><b>{{ i18n "pages.inbounds.oneClick.linkCreated" }}</b></p>
                    <a-input-search
                        :value="inboundLink"
                        read-only
                        :enter-button="`{{ i18n "copy" }}`"
                        @search="copyLink(inboundLink)"
                    />
                </div>
            </a-spin>
        </a-tab-pane>

        <a-tab-pane v-if="linkHistory.length > 0" key="2" :tab="`{{ i18n "pages.inbounds.oneClick.historyTab" }}`">
            <div style="padding: 24px;">
                <a-table 
                    :dataSource="linkHistory" 
                    :pagination="false" 
                    :columns="linkHistoryColumns" 
                    rowKey="time"
                >
                    <template #bodyCell="{ column, record }">
                        <template v-if="column.dataIndex === 'link'">
                            <a-input-search
                                :value="record.link"
                                read-only
                                :enter-button="`{{ i18n "copy" }}`"
                                @search="copyLink(record.link)"
                            />
                           </template>
                       </template>
                   </a-table>
                </div>
            </a-tab-pane>
         </a-tabs>
      </a-modal>
    </a-layout-content>
  </a-layout>
</a-layout>
{{template "page/body_scripts" .}}
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
{{template "component/aSidebar" .}}
{{template "component/aThemeSwitch" .}}
{{template "component/aCustomStatistic" .}}
{{template "component/aPersianDatepicker" .}}
{{template "modals/inboundModal"}}
{{template "modals/promptModal"}}
{{template "modals/qrcodeModal"}}
{{template "modals/textModal"}}
{{template "modals/inboundInfoModal"}}
{{template "modals/clientsModal"}}
{{template "modals/clientsBulkModal"}}
<script>
    const columns = [{
        title: "ID",
        align: 'center',
        dataIndex: "id",
        width: 40,
        responsive: ["xs"],
    }, {
        title: '{{ i18n "pages.inbounds.operate" }}',
        align: 'center',
        width: 30,
        scopedSlots: { customRender: 'action' },
    }, {
        title: '{{ i18n "pages.inbounds.enable" }}',
        align: 'center',
        width: 30,
        scopedSlots: { customRender: 'enable' },
    }, {
        title: '{{ i18n "pages.inbounds.remark" }}',
        align: 'center',
        width: 50,
        dataIndex: "remark",
    }, {
        title: '{{ i18n "pages.inbounds.port" }}',
        align: 'center',
        dataIndex: "port",
        width: 40,
    }, {
        title: '{{ i18n "pages.inbounds.protocol" }}',
        align: 'center',
        width: 70,
        scopedSlots: { customRender: 'protocol' },
    }, {
        title: '{{ i18n "clients" }}',
        align: 'left',
        width: 40,
        scopedSlots: { customRender: 'clients' },
    }, {
       // 【中文注释】: 这是修改后的“设备限制”列定义
        title: '{{ i18n "pages.inbounds.deviceLimit" }}',
        dataIndex: "deviceLimit",
        align: "center",
        width: 40,
       // 【中文注释】: 将错误的 customRender 改为 scopedSlots
        scopedSlots: { customRender: 'deviceLimit' },
    }, {
        title: '{{ i18n "pages.inbounds.traffic" }}',
        align: 'center',
        width: 50,
        scopedSlots: { customRender: 'traffic' },
    }, {
        title: '{{ i18n "pages.inbounds.expireDate" }}',
        align: 'center',
        width: 40,
        scopedSlots: { customRender: 'expiryTime' },
    }];

    const mobileColumns = [{
        title: "ID",
        align: 'right',
        dataIndex: "id",
        width: 10,
        responsive: ["s"],
    }, {
        title: '{{ i18n "pages.inbounds.operate" }}',
        align: 'center',
        width: 25,
        scopedSlots: { customRender: 'action' },
    }, {
        title: '{{ i18n "pages.inbounds.remark" }}',
        align: 'left',
        width: 70,
        dataIndex: "remark",
    }, {
        title: '{{ i18n "pages.inbounds.info" }}',
        align: 'center',
        width: 10,
        scopedSlots: { customRender: 'info' },
    }];

    const innerColumns = [
        { title: '{{ i18n "pages.inbounds.operate" }}', width: 65, scopedSlots: { customRender: 'actions' } },
        { title: '{{ i18n "pages.inbounds.enable" }}', width: 35, scopedSlots: { customRender: 'enable' } },
        { title: '{{ i18n "online" }}', width: 35, scopedSlots: { customRender: 'online' } },
        { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
        { title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
        { title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'allTime' } },

		// 中文注释: 在子表格中新增“限速”列
        {
            title: '{{ i18n "pages.inbounds.speedLimit" }}',
            width: 80,
            align: 'center',
            dataIndex: 'speedLimit',
            customRender: (text) => (text > 0 ? `${text} KB/s` : {{ i18n "pages.inbounds.unlimited" }}),
        },

        { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
    ];

    const innerMobileColumns = [
        { title: '{{ i18n "pages.inbounds.operate" }}', width: 10, align: 'center', scopedSlots: { customRender: 'actionMenu' } },
        { title: '{{ i18n "pages.inbounds.client" }}', width: 90, align: 'left', scopedSlots: { customRender: 'client' } },
        { title: '{{ i18n "pages.inbounds.info" }}', width: 10, align: 'center', scopedSlots: { customRender: 'info' } },
    ];

    const app = new Vue({
        delimiters: ['[[', ']]'],
        el: '#app',
        mixins: [MediaQueryMixin],
        data: {
            themeSwitcher,
            persianDatepicker,
            loadingStates: {
              fetched: false,
              spinning: false
            },
            inbounds: [],
            dbInbounds: [],
            searchKey: '',
            enableFilter: false,
            filterBy: '',
            searchedInbounds: [],
            expireDiff: 0,
            trafficDiff: 0,
            defaultCert: '',
            defaultKey: '',
            clientCount: [],
            onlineClients: [],
            lastOnlineMap: {},
            isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
            refreshing: false,
            refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
            subSettings: {
                enable : false,
                subTitle : '',
                subURI : '',
                subJsonURI : '',
            },
            remarkModel: '-ieo',
            datepicker: 'gregorian',
            tgBotEnable: false,
            showAlert: false,
            ipLimitEnable: false,
            pageSize: 50,

			inboundLink: '', // 新增：用来存储生成的可复制链接
            // 【中文注释】: 新增数据属性，用于控制“一键配置”模态框的显示状态
            oneClickModalVisible: false,

			    // 【新增】: 用于存储最近生成的链接记录
                linkHistory: [],

                // 【新增】: 定义链接历史记录表格的列
                linkHistoryColumns: [
                    {
                        title: `{{ i18n "pages.inbounds.oneClick.historyType" }}`,
                        dataIndex: 'type',
                        width: 150,
                    },
                    {
                        title: `{{ i18n "pages.inbounds.oneClick.historyLink" }}`,
                        dataIndex: 'link',
                    },
                    {
                        title: `{{ i18n "pages.inbounds.oneClick.historyTime" }}`,
                        dataIndex: 'time',
                        width: 220,
                    },
                ],
            // 【中文注释】: 新增数据属性，用于存储“一键配置”模态框中选择的预设值
            oneClickPreset: 'vless_reality',
            // 【中文注释】: 新增Reality SNI的备选列表，用于随机选择
            realityDestinations: [
                'tesla.com:443',
                'sega.com:443',
                'apple.com:443',
                'icloud.com:443',
                'lovelive-anime.jp:443',
                'meta.com:443',
            ],
        },
		
        mounted() {
            // 【中文注释】：这是唯一且正确的 mounted 钩子。
            // 【修改】：将两个 mounted 钩子的逻辑合并到这里。

            // 检查非 https 协议，用于安全提示
            if (window.location.protocol !== "https:") {
                this.showAlert = true;
            }

            // 初始化面板设置和入站列表
            this.getDefaultSettings();
            if (this.isRefreshEnabled) {
                this.startDataRefreshLoop();
            } else {
                this.getDBInbounds();
            }
            
            // 【核心修改】: 从后端加载历史记录，而不是从 localStorage
            this.loadHistoryFromServer();
        },
		
        methods: {
            loading(spinning = true) {
                this.loadingStates.spinning = spinning;
            },
            async getDBInbounds() {
                this.refreshing = true;
                const msg = await HttpUtil.get('/panel/api/inbounds/list');
                if (!msg.success) {
                    this.refreshing = false;
                    return;
                }

                await this.getLastOnlineMap();
                await this.getOnlineUsers();

                this.setInbounds(msg.obj);
                setTimeout(() => {
                    this.refreshing = false;
                }, 500);
            },
            async getOnlineUsers() {
                const msg = await HttpUtil.post('panel/api/inbounds/onlines');
                if (!msg.success) {
                    return;
                }
                this.onlineClients = msg.obj != null ? msg.obj : [];
            },
            async getLastOnlineMap() {
                const msg = await HttpUtil.post('/panel/api/inbounds/lastOnline');
                if (!msg.success || !msg.obj) return;
                this.lastOnlineMap = msg.obj || {}
            },
            async getDefaultSettings() {
                const msg = await HttpUtil.post('/panel/setting/defaultSettings');
                if (!msg.success) {
                    return;
                }
                with(msg.obj){
                    this.expireDiff = expireDiff * 86400000;
                    this.trafficDiff = trafficDiff * 1073741824;
                    this.defaultCert = defaultCert;
                    this.defaultKey = defaultKey;
                    this.tgBotEnable = tgBotEnable;
                    this.subSettings = {
                        enable : subEnable,
                        subTitle : subTitle,
                        subURI: subURI,
                        subJsonURI: subJsonURI
                    };
                    this.pageSize = pageSize;
                    this.remarkModel = remarkModel;
                    this.datepicker = datepicker;
                    this.ipLimitEnable = ipLimitEnable;
                }
            },
            setInbounds(dbInbounds) {
                this.inbounds.splice(0);
                this.dbInbounds.splice(0);
                this.clientCount.splice(0);
                for (const inbound of dbInbounds) {
                    const dbInbound = new DBInbound(inbound);
                    to_inbound = dbInbound.toInbound()
                    this.inbounds.push(to_inbound);
                    this.dbInbounds.push(dbInbound);
                    if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
                        if (dbInbound.isSS && (!to_inbound.isSSMultiUser)) {
                            continue;
                        }
                        this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
                    }
                }
                if (!this.loadingStates.fetched) {
                    this.loadingStates.fetched = true
                }
                if(this.enableFilter){
                    this.filterInbounds();
                } else {
                    this.searchInbounds(this.searchKey);
                }
            },
            getClientCounts(dbInbound, inbound) {
                let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = [], comments = new Map();
                clients = inbound.clients;
                clientStats = dbInbound.clientStats
                now = new Date().getTime()
                if (clients) {
                    clientCount = clients.length;
                    if (dbInbound.enable) {
                        clients.forEach(client => {
                            if (client.comment) {
                              comments.set(client.email, client.comment)
                            }
                            if (client.enable) {
                                active.push(client.email);
                                if (this.isClientOnline(client.email)) online.push(client.email);
                            } else {
                                deactive.push(client.email);
                            }
                        });
                        clientStats.forEach(client => {
                            if (!client.enable) {
                                depleted.push(client.email);
                            } else {
                                if ((client.expiryTime > 0 && (client.expiryTime - now < this.expireDiff)) ||
                                    (client.total > 0 && (client.total - (client.up + client.down) < this.trafficDiff))) expiring.push(client.email);
                            }
                        });
                    } else {
                        clients.forEach(client => {
                            deactive.push(client.email);
                        });
                    }
                }
                return {
                    clients: clientCount,
                    active: active,
                    deactive: deactive,
                    depleted: depleted,
                    expiring: expiring,
                    online: online,
                    comments: comments,
                };
            },

            searchInbounds(key) {
                if (ObjectUtil.isEmpty(key)) {
                    this.searchedInbounds = this.dbInbounds.slice();
                } else {
                    this.searchedInbounds.splice(0, this.searchedInbounds.length);
                    this.dbInbounds.forEach(inbound => {
                        if (ObjectUtil.deepSearch(inbound, key)) {
                            const newInbound = new DBInbound(inbound);
                            const inboundSettings = JSON.parse(inbound.settings);
                            if (inboundSettings.hasOwnProperty('clients')) {
                                const searchedSettings = { "clients": [] };
                                inboundSettings.clients.forEach(client => {
                                    if (ObjectUtil.deepSearch(client, key)) {
                                        searchedSettings.clients.push(client);
                                    }
                                });
                                newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, searchedSettings);
                            }
                            this.searchedInbounds.push(newInbound);
                        }
                    });
                }
            },
            filterInbounds() {
                if (ObjectUtil.isEmpty(this.filterBy)) {
                    this.searchedInbounds = this.dbInbounds.slice();
                } else {
                    this.searchedInbounds.splice(0, this.searchedInbounds.length);
                    this.dbInbounds.forEach(inbound => {
                        const newInbound = new DBInbound(inbound);
                        const inboundSettings = JSON.parse(inbound.settings);
                        if (this.clientCount[inbound.id] && this.clientCount[inbound.id].hasOwnProperty(this.filterBy)){
                            const list = this.clientCount[inbound.id][this.filterBy];
                            if (list.length > 0) {
                                const filteredSettings = { "clients": [] };
                                if (inboundSettings.clients) {
                                    inboundSettings.clients.forEach(client => {
                                        if (list.includes(client.email)) {
                                            filteredSettings.clients.push(client);
                                        }
                                    });
                                }
                                newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);
                                this.searchedInbounds.push(newInbound);
                            }
                        }
                    });
                }
            },
            toggleFilter(){
                if(this.enableFilter) {
                    this.searchKey = '';
                } else {
                    this.filterBy = '';
                    this.searchedInbounds = this.dbInbounds.slice();
                }
            },

			// 【新增函数】: 专门用于调用后端 API 放行端口的方法
async openPort(port) {
    if (!port || port <= 0) {
        this.$message.error('无效端口号，无法放行。');
        return false;
    }

    const loadingMessage = this.$message.loading(`正在向后端发送放行端口 ${port} 的指令...`, 0);
    let success = true;
    
    try {
        // 〔中文注释〕: 调用后端API，它现在会立刻返回一个表示“指令已发送”的JSON。
        const msg = await HttpUtil.postForm('/panel/api/server/openPort', { port: port });
        
        if (msg.success) {
            // 〔中文注释〕: 将提示信息修改为更准确的“指令已发送”，因为任务正在后台运行。
            this.$message.success(`端口 ${port} 放行指令已发送，后台执行中！`);
        } else {
            success = false;
            // 〔中文注释〕: 如果API调用本身就失败了（例如后端服务挂了），则显示错误。
            this.$message.error(`发送端口 ${port} 放行指令失败: ${msg.msg || '未知错误，请检查后端日志。'}`);
        }
    } catch (err) {
        success = false;
        console.error(`请求放行端口 ${port} 失败:`, err);
        // 〔中文注释〕: 网络错误或返回了非JSON内容（例如502网关错误）会在这里捕获。
        this.$message.error(`请求放行端口 ${port} 失败，请检查网络或后端服务状态。 ${err.message}`);
    } finally {
        loadingMessage(); // 无论成功或失败，都关闭加载提示
    }
    return success;
},

			// 〔中文注释〕: 新增一个专门用于生成“一键配置”二维码的方法
            generateOneClickQrCode(link) {
                // 〔中文注释〕: this.$nextTick 是一个 Vue 特有的功能，它会等待 DOM 更新完成后再执行内部的代码。
                // 这是必需的，因为 <canvas> 元素只有在 inboundLink 被赋值后才会显示出来，我们需要确保它存在才能进行绘制。
                this.$nextTick(() => {
                    const canvasElement = document.getElementById('oneClickQrcodeCanvas');
                    if (canvasElement) {
                        new QRious({
                            element: canvasElement, // 〔中文注释〕: 关联到我们刚刚在 HTML 中添加的 canvas 元素
                            value: link,            // 〔中文注释〕: 二维码的内容就是生成的分享链接
                            size: 200,              // 〔中文注释〕: 设置二维码的尺寸为 200x200 像素
                            level: 'M'              // 〔中文注释〕: 设置较高的容错率
                        });
                    }
                });
            },
			
    // 【修改后】: 按照固定长度 2,4,6,8,10,12,14,16 来生成 shortIds
    generateShortIds() {
        const chars = "0123456789abcdef"; // 只使用十六进制字符
        const lengths = [2, 4, 6, 8, 10, 12, 14, 16];
        const shortIds = [];
        for (const len of lengths) {
            let shortId = "";
            for (let j = 0; j < len; j++) {
                shortId += chars[Math.floor(Math.random() * chars.length)];
            }
            shortIds.push(shortId);
        }
        return shortIds;
    },

         // 【中文注释】: 添加这个新函数
            async loadHistoryFromServer() {
               const msg = await HttpUtil.get('/panel/api/server/history/load');
                  if (msg.success && Array.isArray(msg.obj)) {
                 // 后端返回的是标准对象，我们需要转换成前端需要的格式
                     this.linkHistory = msg.obj.map(item => ({
                        type: item.Type,
                        link: item.Link,
                        time: new Date(item.CreatedAt).toLocaleString()
                  }));
                }
             },
			
            // 【中文注释】: 保存链接到历史记录，现在是调用后端 API
            async saveLinkToHistory(type, link) {
            // 1. 在前端立即更新UI，提供即时反馈
                const newRecord = { type: type, link: link, time: new Date().toLocaleString() };
                this.linkHistory.unshift(newRecord);   // 添加到数组开头
                if (this.linkHistory.length > 10) {
                    this.linkHistory.pop();     // 保持最多10条记录
                }

            // 2. 发送数据到后端进行持久化保存，发送表单格式，而不是 JSON
             await HttpUtil.postForm('/panel/api/server/history/save', { type: type, link: link });
            },

            // 【中文注释】: 以下是新增和修改后的方法，用于处理“一键配置”和“订阅转换”
            // 【中文注释】: 打开“一键配置”模态框
            openOneClickConfig() {
                this.oneClickModalVisible = true;
            },

            // 【中文注释】: 处理“一键配置”模态框的确认操作
            async handleOneClickOk(presetType) {
                this.loadingStates.spinning = true;
                
               // 【中文注释】: 根据选择的预设值调用不同的创建函数
                if (presetType === 'vless_reality') {
                    await this.createVlessRealityInbound();
                } else if (presetType === 'vless_tls_encryption') {
                    await this.createVlessTlsEncryptionInbound();
                } else if (presetType === 'switch') {
                    this.$message.info('Switch 模式配置尚未实现');
                } else {
                    this.$message.error('未知的预设配置！');
                }

               // 【中文注释】: 无论如何，最后都关闭加载状态
                this.loadingStates.spinning = false;
            },

            // 【中文注释】: 处理“一键配置”模态框的取消操作
            handleOneClickCancel() {
                this.oneClickModalVisible = false;
                this.inboundLink = '';          // 关闭时清空链接，以便下次重新打开时显示选项
            },

            // 【中文注释】: 创建 VLESS + Reality 入站的具体实现
            async createVlessRealityInbound() {
                try {
                     // 【中文注释】: 使用 Promise.all 并行请求后端的两个接口，以获取密钥对和UUID
	    // 【重要】: 这里调用的都是项目中已经存在的后端接口
                    const [keyPairMsg, uuidMsg] = await Promise.all([
                        HttpUtil.get('/panel/api/server/getNewX25519Cert'),
                        HttpUtil.get('/panel/api/server/getNewUUID')
                    ]);

                    if (!keyPairMsg.success || !keyPairMsg.obj || !uuidMsg.success || !uuidMsg.obj) {
                        this.$message.error('无法从服务器获取 Reality 密钥对或 UUID，请检查后端服务！');
                        return;
                    }

                    const { privateKey, publicKey } = keyPairMsg.obj;
                    const { uuid } = uuidMsg.obj;
                    const remark = Array.from({length:8}, () => 
                        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
                        [Math.floor(Math.random() * 62)]
                    ).join("");

                    // 随机生成 10000-55535 之间的端口
                    const port = Math.floor(Math.random() * (55535 - 10000 + 1)) + 10000;

					// 【新增代码】: 自动调用 openPort 接口放行端口
                    const portOpenSuccess = await this.openPort(port);
                    if (!portOpenSuccess) {
                       // 【不中断流程，给出警告提示】
                       this.$message.warning(`**注意：** 自动放行端口 ${port} 失败，请务必手动到您的 **VPS 防火墙**（例如 ufw、security group）放行该端口，否则入站将无法连接。`);
                    }
        
	                // 【中文注释】: 从预设列表中随机选择一个SNI
                    const randomDest = this.realityDestinations[Math.floor(Math.random() * this.realityDestinations.length)];
          
                    // 从 randomDest (例如 'microsoft.com:443') 中提取出域名 ('microsoft.com')
                    const randomSni = randomDest.split(':')[0];

                    // 【随机生成】: 调用新的函数 generateShortIds()
                    const shortIds = this.generateShortIds();
                    const linkSid = shortIds[Math.floor(Math.random() * shortIds.length)];

                    const data = {
                        up: 0,
                        down: 0,
                        total: 0,
                        remark: remark,
                        enable: true,
                        expiryTime: 0,
						deviceLimit: 0, 
                        listen: '',
                        port: port,
                        protocol: 'vless',
                        settings: JSON.stringify({
                            clients: [{
                                id: uuid,           // 【中文注释】: 使用后端生成的 UUID
                                flow: "xtls-rprx-vision",
                                email: remark,
                                level: 0,
                                enable: true
                            }],
                            decryption: "none",
                            fallbacks: []
                        }),
                        streamSettings: JSON.stringify({
                            network: "tcp",
                            security: "reality",
                            realitySettings: {
                                show: false,
                                target: randomDest,
                                xver: 0,
                                serverNames: [ randomSni, `www.${randomSni}` ],
								settings: {
                                    publicKey: publicKey,
                                    spiderX: "/",
                                    mldsa65Verify: ""
                                },
								privateKey: privateKey,
                                maxClientVer: "",
                                minClientVer: "",
                                maxTimediff: 0,
                                mldsa65Seed: "",         // 【中文注释】: 一般情况下【留空】
                                "shortIds": shortIds
                            },
                            tcpSettings: {
                                acceptProxyProtocol: false,
                                header: {
                                    type: "none"
                                }
                            }
                        }),

	       // 【中文注释】：构建完整的 sniffing 对象
                        sniffing: JSON.stringify({
                            enabled: true,
                            destOverride: ["http", "tls", "quic", "fakedns"],
                            metadataOnly: false,
                            routeOnly: false
                        }),
                    };
                    
                    const msg = await HttpUtil.post('/panel/api/inbounds/add', data);

                    if (msg.success) {
                        const link = `vless://${uuid}@${window.location.hostname}:${port}?type=tcp&encryption=none&security=reality&pbk=${publicKey}&fp=chrome&sni=${randomSni}&sid=${linkSid}&spx=%2F&flow=xtls-rprx-vision#${remark}-${remark}`;
                        this.inboundLink = link;

						// 〔中文注释〕: 在这里调用新增的二维码生成方法，把链接传进去
                        this.generateOneClickQrCode(link);
                        
                        // 【新增】: 调用保存方法，将链接保存到历史记录
                        this.saveLinkToHistory('vless_reality', link);
                        
                        this.$message.success('一键配置入站创建成功!');
                        await this.getDBInbounds();
                    } else {
                        // 如果后端返回失败，显示后端的消息
                        this.$message.error('创建失败: ' + msg.msg);
                    }
                } catch (err) {
                    console.error("创建一键配置入站时出错:", err);
                    this.$message.error('创建过程中发生网络错误，请检查后端日志。');
                }
            },

            // 【方法】: 创建 VLESS + TLS + Encryption 的入站配置
            async createVlessTlsEncryptionInbound() {
                try {
                    const [encMsg, uuidMsg] = await Promise.all([
                        HttpUtil.get('/panel/api/server/getNewVlessEnc'),
                        HttpUtil.get('/panel/api/server/getNewUUID')
                    ]);

                    // 【修正】: 增加对 API 返回数据的严格检查
                    if (!encMsg.success || !encMsg.obj || !Array.isArray(encMsg.obj.auths) || encMsg.obj.auths.length === 0 || !uuidMsg.success || !uuidMsg.obj) {
                         this.$message.error('无法从服务器获取 Vless 加密配置或 UUID，请检查后端服务！');
                         return;
                    }

					// 【修正】: 遍历找到正确的 ML-KEM-768 密钥
                    const mlkemAuth = encMsg.obj.auths.find(auth => auth.label === "ML-KEM-768, Post-Quantum");

                    if (!mlkemAuth || !mlkemAuth.decryption || !mlkemAuth.encryption) {
                         this.$message.error('未能从服务器获取到 ML-KEM-768 加密密钥，请检查 Xray 版本！');
                         return;
                    }
                    // 【修正】: 正确从嵌套数据中解构出 decryption 和 encryption
                    const { decryption, encryption } = mlkemAuth;

                    const { uuid } = uuidMsg.obj;
                    const remark = Array.from({length:8}, () => 
                        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
                        [Math.floor(Math.random() * 62)]
                    ).join("");

                    // 随机生成2053, 2083, 2087, 2096, 8443这5个端口中的其中一个
                    const allowedPorts = [2053, 2083, 2087, 2096, 8443];
                    const port = allowedPorts[Math.floor(Math.random() * allowedPorts.length)];

					// 【新增代码】: 自动调用 openPort 接口放行端口
                    const portOpenSuccess = await this.openPort(port);
                    if (!portOpenSuccess) {
                    // 【不中断流程，给出警告提示】
                        this.$message.warning(`**注意：** 自动放行端口 ${port} 失败，请务必手动到您的 **VPS 防火墙**（例如 ufw、security group）放行该端口，否则入站将无法连接。`);
                    }
                    
					// 随机生成一个8位大小写字母的路径
					const path = "/" + Array.from({ length: 8 }, () =>
                        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[Math.floor(Math.random() * 52)]
                         ).join("");

					// 固定使用面板域名作为 SNI
                    const serverName = window.location.hostname; 

                    // 【必须】: 动态设置证书路径
                    const certPath = `/root/cert/${window.location.hostname}/fullchain.pem`;
                    const keyPath = `/root/cert/${window.location.hostname}/privkey.pem`;
                    
                    const data = {
                        up: 0,
                        down: 0,
                        total: 0,
                        remark: remark,
                        enable: true,
                        expiryTime: 0,
						deviceLimit: 0, 
                        listen: '',
                        port: port,
                        protocol: 'vless',
                        settings: JSON.stringify({
                            clients: [{
                                id: uuid,        // 【中文注释】: 使用后端生成的 UUID
                                flow: "", 
                                email: remark,
                                level: 0,
								password: "",
                                enable: true
                            }],
                            decryption: decryption, // 使用从 API 获取的 decryption
                            encryption: encryption, // 【重要】: 加上 encryption 参数
                            selectedAuth: "ML-KEM-768, Post-Quantum"
                        }),
                        streamSettings: JSON.stringify({
                            network: "xhttp",       // 使用 XHTTP 传输
                            security: "tls",
                            tlsSettings: {
                                alpn: ["h2", "http/1.1"],   // 优先 h2，再 http/1.1
                                certificates: [{
                                    buildChain: false,
                                    certificateFile: certPath,
                                    keyFile: keyPath,
                                    oneTimeLoading: false,
                                    usage: "encipherment"
                                }],
                                cipherSuites: "",
                                disableSystemRoot: false,
                                echForceQuery: "none",
                                echServerKeys: "",
                                enableSessionResumption: false,
                                maxVersion: "1.3",
                                minVersion: "1.2",
                                rejectUnknownSni: false,
                                serverName: serverName, // 使用面板域名的 SNI
                                verifyPeerCertInNames: ["dns.google", "cloudflare-dns.com"]
                           },
						   xhttpSettings: {
                                headers: {},                 // 可按需填充
                                host: "",                    // 一般默认保持空
                                mode: "packet-up",           // packet-up 分包上行
                                noSSEHeader: false,
                                path: path,                  // 随机 8 位大小写字母路径
                                scMaxBufferedPosts: 30,
                                scMaxEachPostBytes: "1000000",
                                scStreamUpServerSecs: "20-80",
                                xPaddingBytes: "100-1000"
                           }
                        }),
                        sniffing: JSON.stringify({
                            enabled: false,
                            destOverride: ["http", "tls", "quic", "fakedns"],
                            metadataOnly: false,
                            routeOnly: false
                        }),
                    };
                    
                    const msg = await HttpUtil.post('/panel/api/inbounds/add', data);

                    if (msg.success) {
                        // 【重要】: 分享链接格式，添加 encryption 和 flow
                        const link = `vless://${uuid}@${window.location.hostname}:${port}?type=tcp&encryption=${encryption}&security=tls&fp=chrome&alpn=http%2F1.1&sni=${serverName}&flow=xtls-rprx-vision#${remark}-${remark}`;
                        this.inboundLink = link;

						// 〔中文注释〕: 在这里调用新增的二维码生成方法，把链接传进去
                        this.generateOneClickQrCode(link);

                        // 【新增】: 调用保存方法，将链接保存到历史记录
                        this.saveLinkToHistory('vless_tls_encryption', link);
                        
                        this.$message.success('VLESS + TLS + Encryption 配置创建成功!');
                        await this.getDBInbounds();
                    } else {
                        this.$message.error('创建失败: ' + msg.msg);
                    }
                } catch (err) {
                    console.error("创建 VLESS + TLS + Encryption 入站时出错:", err);
                    this.$message.error('创建过程中发生网络错误，请检查后端日志。');
                }
            },

            // 【中文注释】: 这是修复后的 copyLink 方法
            async copyLink(linkToCopy) {
                const link = linkToCopy || this.inboundLink;
                if (!link) {
                    this.$message.warning('没有可复制的链接。');
                    return;
                }

                // 【中文注释】: 优先使用 Clipboard API，但它需要安全上下文 (HTTPS)
                if (navigator.clipboard && window.isSecureContext) {
                    try {
                        await navigator.clipboard.writeText(link);
                        this.$message.success('链接已成功复制到剪贴板！');
                        return;
                    } catch (err) {
                        console.error('Clipboard API 复制失败，将尝试回退方法:', err);
                        // 如果失败，则继续执行下面的回退方法
                    }
                }

                // 【中文注释】: 回退方案，使用 document.execCommand，兼容性更好
                const textArea = document.createElement("textarea");
                textArea.value = link;
                // 将输入框移动到屏幕外，防止干扰
                textArea.style.position = "absolute";
                textArea.style.left = "-9999px";
                textArea.setAttribute("readonly", "");

                document.body.appendChild(textArea);
                
                // 选择输入框中的内容
                textArea.select();
                
                try {
                    // 执行复制命令
                    document.execCommand('copy');
                    this.$message.success('链接已成功复制到剪贴板！');
                } catch (err) {
                    console.error('execCommand 复制失败:', err);
                    this.$message.error('复制失败，请手动复制。');
                } finally {
                    // 无论成功与否，最后都移除这个临时输入框
                    document.body.removeChild(textArea);
                }
            },


            // 〔中文注释〕: 处理“订阅转换”按钮的点击事件 (已更新为完整逻辑)
            handleSubscriptionConversion() {
                // 【第一步】: 检查当前协议是否为 HTTPS
                if (window.location.protocol !== "https:") {
                    this.$warning({
                        title: '必须使用 HTTPS 访问',
                        content: '【订阅转换】功能需要一个安全的 HTTPS 环境才能运行。请先为您的面板域名正确配置 SSL 证书，然后再使用此功能。',
                        class: themeSwitcher.currentTheme,
                    });
                    return; // 如果是 http，则终止后续所有操作
                }

                // 【第二步】：弹出确认框，询问用户是否继续
                this.$confirm({
                    title: '{{ i18n "pages.inbounds.subConversion.modalTitle"}}',
                    content: '{{ i18n "pages.inbounds.subConversion.modalContent"}}',
                    okText: '{{ i18n "pages.inbounds.subConversion.modalOk"}}',
                    cancelText: '{{ i18n "cancel" }}',
                    class: themeSwitcher.currentTheme,
                    onOk: () => {
                        const subConverterUrl = `https://${window.location.hostname}:15268`;
                        const loadingMessage = this.$message.loading('正在检测订阅转换服务 (运行端口15268)...', 0);

                        const controller = new AbortController();
                        const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时检测

                        // 【第三步】: 尝试通过 fetch API 访问订阅转换服务端口
                        fetch(subConverterUrl, {
                            method: 'GET',
                            mode: 'no-cors', // 〔中文注释〕: 使用 no-cors 模式进行简单的网络连通性检查
                            signal: controller.signal
                        })
                        .then(response => {
                            // 【情况 A】: 访问成功，服务正在运行
                            clearTimeout(timeoutId);
                            loadingMessage(); // 关闭加载提示
                            this.$message.success('服务检测成功，正在为您跳转...', 2);
                            window.open(subConverterUrl, '_blank'); // 在新标签页中打开
                        })
                        .catch(err => {
                            // 【情况 B】: 访问失败 (例如超时或连接被拒绝)
                            clearTimeout(timeoutId);
                            loadingMessage(); // 关闭加载提示
                            
                            // 〔中文注释〕: 弹出新的确认框，提供“一键安装”选项
                            this.$confirm({
                                title: '{{ i18n "pages.inbounds.subConversion.notFoundTitle"}}',
                                content: `无法连接到订阅转换服务。这可能意味着它尚未安装或未在运行。您想现在远程执行安装指令吗？

                                         【重要提示】: 执行前，请确保您服务器的防火墙已放行 8000 和 15268 这两个端口。`,
                                okText: '立即安装',
                                cancelText: '稍后再说',
                                class: themeSwitcher.currentTheme,
                                onOk: async () => {
                                    // 〔中文注释〕：从这里开始是全新的轮询检测逻辑
									// 确保 loginUrl 被定义，用于成功提示
                                    const subConverterUrl = `https://${window.location.hostname}:15268`;
                                    const loginUrl = subConverterUrl; 
									
									const portsToOpen = [8000, 15268];
                                    let openPortFailed = false; // 用于标记是否有端口放行失败
                        
                                    // 【新增逻辑】：先尝试放行 8000 和 15268 端口 (不中断流程)
                                    const openPortLoading = this.$message.loading(`正在尝试自动放行端口 ${portsToOpen.join(' 和 ')}...`, 0);

                                    // 使用 Promise.allSettled 确保所有端口放行尝试完成后才继续
                                    const openPortResults = await Promise.allSettled(
                                        portsToOpen.map(port => this.openPort(port))
                                    );
                        
                                    openPortLoading(); // 关闭放行加载提示
                        
                                    openPortResults.forEach(result => {
                                    // 检查每个放行操作是否失败 (openPort 返回 false)
                                    if (result.status === 'fulfilled' && result.value === false) {
                                        openPortFailed = true;
                                    }
                                    // Promise.allSettled 的 rejected 状态表明 openPort 内部抛出异常 (如网络错误)，也视为放行失败
                                    if (result.status === 'rejected') {
                                        openPortFailed = true;
                                     }
                                });

                                    if (openPortFailed) {
                                   // 使用非模态、自动关闭的 this.$message.warning
                                    this.$message.warning({
                                        content: `端口自动放行失败。**安装流程将继续**，但请您务必进入 VPS 手动放行 8000 和 15268 端口。`,
                                        duration: 10, // 持续 10 秒后自动关闭
                                        key: 'portOpenWarning', // 可选：指定一个key，防止重复弹出
                                    });
                                  }
                                    // 1. 先向后端发送安装指令
                                    const installLoading = this.$message.loading('正在向后端发送安装指令...', 0);
                                    try {
                                        const msg = await HttpUtil.post('/panel/api/server/install/subconverter');
                                        installLoading(); // 指令发送成功后，关闭这个短暂的加载提示
                                        if (!msg.success) {
                                            this.$error({
                                                title: '指令发送失败',
                                                content: msg.msg || '发生未知错误，请检查后端日志。',
                                                class: themeSwitcher.currentTheme,
                                            });
                                            return;
                                        }
                                    } catch (error) {
                                        installLoading();
                                        this.$error({
                                            title: '前端请求失败',
                                            content: '无法连接到后端API接口，请检查您的网络连接或后端服务是否正常运行。',
                                            class: themeSwitcher.currentTheme,
                                        });
                                        return;
                                    }

                                    // 2. 指令发送成功后，创建并显示一个用于等待服务启动的模态框
                                    const h = this.$createElement;
                                    const modal = this.$info({
                                    title: '⏳ 正在安装并启动服务...',
                                    content: '安装指令已发送，后台正在执行安装和启动。请耐心等待，预计40秒内自动给出安装成功提示。',
                                    class: themeSwitcher.currentTheme,
                                    okText: '后台运行',
                                    okButtonProps: {
                                         props: {
                                            loading: true // 让按钮一直处于加载状态
                                         }
                                       },
                                     });

                                    // 3. 核心妥协：使用固定延时代替不可靠的前端轮询
                                    const SERVICE_STARTUP_WAIT_TIME = 40000; // 等待 40 秒钟

                                    setTimeout(() => {
                                    // 延时结束后，强制更新模态框为成功状态
                                       modal.update({
                                         title: '✅ 安装成功！',
                                         // 使用 VNode 来创建指定的提示内容
                                         content: h('div', { style: 'white-space: pre-wrap; text-align: left;' }, [
                                         h('p', { style: 'font-weight: bold; font-size: 16px; margin-bottom: 10px;'}, '🎉 恭喜！【订阅转换】模块已成功安装！'),
                                         h('p', '您现在可以使用以下地址访问 Web 界面：'),
                                         h('p', [
                                         h('strong', '🔗 登录地址: '),
                                         h('a', {
                                           attrs: {
                                              href: loginUrl,
                                              target: '_blank'
                                            },
                                         style: 'font-family: monospace; font-weight: bold;'
                                         }, loginUrl)
                                        ]),
                                        // 突出显示用户名和密码
                                        h('p', [ '默认用户名: ', h('strong', 'admin') ]),
                                        h('p', [ '默认 密码: ', h('strong', '123456') ]),
                                        h('p', { style: 'font-size: 12px; color: #888; margin-top: 10px;'}, '可登录订阅转换后台修改您的密码！'),
                                        h('p', { style: 'margin-top: 10px; font-style: italic; color: #ff4d4f;' }, '---------------------------------------'), // 使用红色分隔线
                                        h('p', { style: 'font-style: italic; color: #ff4d4f;' }, '【重要提示】：若您无法访问上述地址，请进入 VPS 中手动放行 8000 和 15268 端口。') // 红色字体
                                        ]),
                                       okText: '完成',
                                       okButtonProps: {
                                            props: {
                                               loading: false
                                            }
                                          },
                                       });
                                    }, SERVICE_STARTUP_WAIT_TIME);
                                 },
                            });
                        });
                    },
                });
            },

            generalActions(action) {
                switch (action.key) {
                    case "import":
                        this.importInbound();
                        break;
                    case "export":
                        this.exportAllLinks();
                        break;
                    case "subs":
                        this.exportAllSubs();
                        break;
                    case "resetInbounds":
                        this.resetAllTraffic();
                        break;
                    case "resetClients":
                        this.resetAllClientTraffics(-1);
                        break;
                    case "delDepletedClients":
                        this.delDepletedClients(-1)
                        break;
                }
            },
            clickAction(action, dbInbound) {
                switch (action.key) {
                    case "qrcode":
                        this.showQrcode(dbInbound.id);
                        break;
                    case "showInfo":
                        this.showInfo(dbInbound.id);
                        break;
                    case "edit":
                        this.openEditInbound(dbInbound.id);
                        break;
                    case "addClient":
                        this.openAddClient(dbInbound.id)
                        break;
                    case "addBulkClient":
                        this.openAddBulkClient(dbInbound.id)
                        break;
                    case "export":
                        this.inboundLinks(dbInbound.id);
                        break;
                    case "subs":
                        this.exportSubs(dbInbound.id);
                        break;
                    case "clipboard":
                        this.copy(dbInbound.id);
                        break;
                    case "resetTraffic":
                        this.resetTraffic(dbInbound.id);
                        break;
                    case "resetClients":
                        this.resetAllClientTraffics(dbInbound.id);
                        break;
                    case "clone":
                        this.openCloneInbound(dbInbound);
                        break;
                    case "delete":
                        this.delInbound(dbInbound.id);
                        break;
                    case "delDepletedClients":
                        this.delDepletedClients(dbInbound.id)
                        break;
                }
            },
            openCloneInbound(dbInbound) {
                this.$confirm({
                    title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"',
                    content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
                    okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}',
                    class: themeSwitcher.currentTheme,
                    cancelText: '{{ i18n "cancel" }}',
                    onOk: () => {
                        const baseInbound = dbInbound.toInbound();
                        dbInbound.up = 0;
                        dbInbound.down = 0;
                        this.cloneInbound(baseInbound, dbInbound);
                    },
                });
            },
            async cloneInbound(baseInbound, dbInbound) {
                const data = {
                    up: dbInbound.up,
                    down: dbInbound.down,
                    total: dbInbound.total,
                    remark: dbInbound.remark + " - Cloned",
                    enable: dbInbound.enable,
                    expiryTime: dbInbound.expiryTime,

                    listen: '',
                    port: RandomUtil.randomInteger(10000, 60000),
                    protocol: baseInbound.protocol,
                    settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
                    streamSettings: baseInbound.stream.toString(),
                    sniffing: baseInbound.sniffing.toString(),
                };
                await this.submit('/panel/api/inbounds/add', data, inModal);
            },
            openAddInbound() {
                inModal.show({
                    title: '{{ i18n "pages.inbounds.addInbound"}}',
                    okText: '{{ i18n "create"}}',
                    cancelText: '{{ i18n "close" }}',
                    confirm: async (inbound, dbInbound) => {
                        await this.addInbound(inbound, dbInbound, inModal);
                    },
                    isEdit: false
                });
            },
            openEditInbound(dbInboundId) {
                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
                const inbound = dbInbound.toInbound();
                inModal.show({
                    title: '{{ i18n "pages.inbounds.modifyInbound"}}',
                    okText: '{{ i18n "update"}}',
                    cancelText: '{{ i18n "close" }}',
                    inbound: inbound,
                    dbInbound: dbInbound,
                    confirm: async (inbound, dbInbound) => {
                        await this.updateInbound(inbound, dbInbound);
                    },
                    isEdit: true
                });
            },
            async addInbound(inbound, dbInbound) {
                const data = {
                    up: dbInbound.up,
                    down: dbInbound.down,
                    total: dbInbound.total,
                    remark: dbInbound.remark,
                    enable: dbInbound.enable,
                    expiryTime: dbInbound.expiryTime,

                   // 新增这一行
                   deviceLimit: dbInbound.deviceLimit,

                    listen: inbound.listen,
                    port: inbound.port,
                    protocol: inbound.protocol,
                    settings: inbound.settings.toString(),
                };
                if (inbound.canEnableStream()){
                  data.streamSettings = inbound.stream.toString();
                } else if (inbound.stream?.sockopt) {
                  data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);
                }
                data.sniffing = inbound.sniffing.toString();

                await this.submit('/panel/api/inbounds/add', data, inModal);
            },
            async updateInbound(inbound, dbInbound) {
                const data = {
                    up: dbInbound.up,
                    down: dbInbound.down,
                    total: dbInbound.total,
                    remark: dbInbound.remark,
                    enable: dbInbound.enable,
                    expiryTime: dbInbound.expiryTime,
                   // 新增这一行
                   deviceLimit: dbInbound.deviceLimit,

                    listen: inbound.listen,
                    port: inbound.port,
                    protocol: inbound.protocol,
                    settings: inbound.settings.toString(),
                };
                if (inbound.canEnableStream()){
                  data.streamSettings = inbound.stream.toString();
                } else if (inbound.stream?.sockopt) {
                  data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);
                }
                data.sniffing = inbound.sniffing.toString();

                await this.submit(`/panel/api/inbounds/update/${dbInbound.id}`, data, inModal);
            },
            openAddClient(dbInboundId) {
                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
                clientModal.show({
                    title: '{{ i18n "pages.client.add"}}',
                    okText: '{{ i18n "pages.client.submitAdd"}}',
                    dbInbound: dbInbound,
                    confirm: async (clients, dbInboundId) => {
                        await this.addClient(clients, dbInboundId, clientModal);
                    },
                    isEdit: false
                });
            },
            openAddBulkClient(dbInboundId) {
                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
                clientsBulkModal.show({
                    title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
                    okText: '{{ i18n "pages.client.bulk"}}',
                    dbInbound: dbInbound,
                    confirm: async (clients, dbInboundId) => {
                        await this.addClient(clients, dbInboundId, clientsBulkModal);
                    },
                });
            },
            openEditClient(dbInboundId, client) {
                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
                clients = this.getInboundClients(dbInbound);
                index = this.findIndexOfClient(dbInbound.protocol, clients, client);
                clientModal.show({
                    title: '{{ i18n "pages.client.edit"}}',
                    okText: '{{ i18n "pages.client.submitEdit"}}',
                    dbInbound: dbInbound,
                    index: index,
                    confirm: async (client, dbInboundId, clientId) => {
                        clientModal.loading();
                        await this.updateClient(client, dbInboundId, clientId);
                        clientModal.close();
                    },
                    isEdit: true
                });
            },
            findIndexOfClient(protocol, clients, client) {
                switch (protocol) {
                    case Protocols.TROJAN:
                    case Protocols.SHADOWSOCKS:
                        return clients.findIndex(item => item.password === client.password && item.email === client.email);
                    default: return clients.findIndex(item => item.id === client.id && item.email === client.email);
                }
            },
            async addClient(clients, dbInboundId, modal) {
                const data = {
                    id: dbInboundId,
                    settings: '{"clients": [' + clients.toString() + ']}',
                };
                await this.submit(`/panel/api/inbounds/addClient`, data, modal);
            },
            async updateClient(client, dbInboundId, clientId) {
                const data = {
                    id: dbInboundId,
                    settings: '{"clients": [' + client.toString() + ']}',
                };
                await this.submit(`/panel/api/inbounds/updateClient/${clientId}`, data, clientModal);
            },
            resetTraffic(dbInboundId) {
                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
                this.$confirm({
                    title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' #' + dbInboundId,
                    content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
                    class: themeSwitcher.currentTheme,
                    okText: '{{ i18n "reset"}}',
                    cancelText: '{{ i18n "cancel"}}',
                    onOk: () => {
                        const inbound = dbInbound.toInbound();
                        dbInbound.up = 0;
                        dbInbound.down = 0;
                        this.updateInbound(inbound, dbInbound);
                    },
                });
            },
            delInbound(dbInboundId) {
                this.$confirm({
                    title: '{{ i18n "pages.inbounds.deleteInbound"}}' + ' #' + dbInboundId,
                    content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
                    class: themeSwitcher.currentTheme,
                    okText: '{{ i18n "delete"}}',
                    cancelText: '{{ i18n "cancel"}}',
                    onOk: () => this.submit('/panel/api/inbounds/del/' + dbInboundId),
                });
            },
            delClient(dbInboundId, client,confirmation = true) {
                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
                clientId = this.getClientId(dbInbound.protocol, client);
                if (confirmation){
                    this.$confirm({
                        title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email,
                        content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
                        class: themeSwitcher.currentTheme,
                        okText: '{{ i18n "delete"}}',
                        cancelText: '{{ i18n "cancel"}}',
                        onOk: () => this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`),
                    });
                } else {
                    this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`);
                }
            },
            getSubGroupClients(dbInbounds, currentClient) {
                const response = {
                  inbounds: [],
                  clients: [],
                  editIds: []
                }
                if (dbInbounds && dbInbounds.length > 0 && currentClient) {
                    dbInbounds.forEach((dbInboundItem) => {
                        const dbInbound = new DBInbound(dbInboundItem);
                        if (dbInbound) {
                            const inbound = dbInbound.toInbound();
                            if (inbound) {
                                const clients = inbound.clients;
                                if (clients.length > 0) {
                                    clients.forEach((client) => {
                                        if (client['subId'] === currentClient['subId']) {
                                            client['inboundId'] = dbInboundItem.id
                                            client['clientId'] = this.getClientId(dbInbound.protocol, client)
                                            response.inbounds.push(dbInboundItem.id)
                                            response.clients.push(client)
                                            response.editIds.push(client['clientId'])
                                        }
                                    })
                                }
                            }
                        }
                    })
                }
                return response;
            },
            getClientId(protocol, client) {
                switch (protocol) {
                    case Protocols.TROJAN: return client.password;
                    case Protocols.SHADOWSOCKS: return client.email;
                    default: return client.id;
                }
            },
            checkFallback(dbInbound) {
                newDbInbound = new DBInbound(dbInbound);
                if (dbInbound.listen.startsWith("@")){
                    rootInbound = this.inbounds.find((i) =>
                        i.isTcp &&
                        ['trojan','vless'].includes(i.protocol) &&
                        i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
                    );
                    if (rootInbound) {
                        newDbInbound.listen = rootInbound.listen;
                        newDbInbound.port = rootInbound.port;
                        newInbound = newDbInbound.toInbound();
                        newInbound.stream.security = rootInbound.stream.security;
                        newInbound.stream.tls = rootInbound.stream.tls;
                        newInbound.stream.externalProxy = rootInbound.stream.externalProxy;
                        newDbInbound.streamSettings = newInbound.stream.toString();
                    }
                }
                return newDbInbound;
            },
            showQrcode(dbInboundId, client) {
                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
                newDbInbound = this.checkFallback(dbInbound);
                qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client);
            },
            showInfo(dbInboundId, client) {
                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
                index=0;
                if (dbInbound.isMultiUser()){
                    inbound = dbInbound.toInbound();
                    clients = inbound.clients;
                    index = this.findIndexOfClient(dbInbound.protocol, clients, client);
                }
                newDbInbound = this.checkFallback(dbInbound);
                infoModal.show(newDbInbound, index);
            },
            switchEnable(dbInboundId,state) {
              dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
              dbInbound.enable = state;
              this.submit(`/panel/api/inbounds/update/${dbInboundId}`, dbInbound);
            },
            async switchEnableClient(dbInboundId, client) {
                this.loading()
                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
                inbound = dbInbound.toInbound();
                clients = inbound.clients;
                index = this.findIndexOfClient(dbInbound.protocol, clients, client);
                clients[index].enable = !clients[index].enable;
                clientId = this.getClientId(dbInbound.protocol, clients[index]);
                await this.updateClient(clients[index], dbInboundId, clientId);
                this.loading(false);
            },
            async submit(url, data, modal) {
                const msg = await HttpUtil.postWithModal(url, data, modal);
                if (msg.success) {
                    await this.getDBInbounds();
                }
            },

            getInboundClients(dbInbound) {
                return dbInbound.toInbound().clients;
            },
			
            resetClientTraffic(client, dbInboundId, confirmation = true) {
                if (confirmation){
                    this.$confirm({
                        title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + client.email,
                        content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
                        class: themeSwitcher.currentTheme,
                        okText: '{{ i18n "reset"}}',
                        cancelText: '{{ i18n "cancel"}}',
                        onOk: () => this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email),
                    })
                } else {
                    this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email);
                }
            },
            resetAllTraffic() {
                this.$confirm({
                    title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
                    content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
                    class: themeSwitcher.currentTheme,
                    okText: '{{ i18n "reset"}}',
                    cancelText: '{{ i18n "cancel"}}',
                    onOk: () => this.submit('/panel/api/inbounds/resetAllTraffics'),
                });
            },
            resetAllClientTraffics(dbInboundId) {
                this.$confirm({
                    title: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
                    content: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
                    class: themeSwitcher.currentTheme,
                    okText: '{{ i18n "reset"}}',
                    cancelText: '{{ i18n "cancel"}}',
                    onOk: () => this.submit('/panel/api/inbounds/resetAllClientTraffics/' + dbInboundId),
                })
            },
            delDepletedClients(dbInboundId) {
                this.$confirm({
                    title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
                    content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
                    class: themeSwitcher.currentTheme,
                    okText: '{{ i18n "delete"}}',
                    cancelText: '{{ i18n "cancel"}}',
                    onOk: () => this.submit('/panel/api/inbounds/delDepletedClients/' + dbInboundId),
                })
            },
            isExpiry(dbInbound, index) {
                return dbInbound.toInbound().isExpiry(index);
            },
            getUpStats(dbInbound, email) {
                if (email.length == 0) return 0;
                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
                return clientStats ? clientStats.up : 0;
            },
            getDownStats(dbInbound, email) {
                if (email.length == 0) return 0;
                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
                return clientStats ? clientStats.down : 0;
            },
            getSumStats(dbInbound, email) {
                if (email.length == 0) return 0;
                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
                return clientStats ? clientStats.up + clientStats.down : 0;
            },
            getAllTimeClient(dbInbound, email) {
                if (email.length == 0) return 0;
                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
                if (!clientStats) return 0;
                return clientStats.allTime || (clientStats.up + clientStats.down);
            },
            getRemStats(dbInbound, email) {
                if (email.length == 0) return 0;
                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
                if (!clientStats) return 0;
                remained = clientStats.total - (clientStats.up + clientStats.down);
                return remained>0 ? remained : 0;
            },
            clientStatsColor(dbInbound, email) {
                if (email.length == 0) return ColorUtils.clientUsageColor();
                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
                return ColorUtils.clientUsageColor(clientStats, app.trafficDiff)
            },
            statsProgress(dbInbound, email) {
                if (email.length == 0) return 100;
                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
                if (!clientStats) return 0;
                if (clientStats.total == 0) return 100;
                return 100*(clientStats.down + clientStats.up)/clientStats.total;
            },
            expireProgress(expTime, reset) {
                now = new Date().getTime();
                remainedSeconds = expTime < 0 ? -expTime/1000 : (expTime-now)/1000;
                resetSeconds = reset * 86400;
                if (remainedSeconds >= resetSeconds) return 0;
                return 100*(1-(remainedSeconds/resetSeconds));
            },
            remainedDays(expTime){
                if (expTime == 0) return null;
                if (expTime < 0) return TimeFormatter.formatSecond(expTime/-1000);
                now = new Date().getTime();
                if (expTime < now) return '{{ i18n "depleted" }}';
                return TimeFormatter.formatSecond((expTime-now)/1000);
            },
            statsExpColor(dbInbound, email){
                if (email.length == 0) return '#7a316f';
                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
                if (!clientStats) return '#7a316f';
                statsColor = ColorUtils.usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
                expColor = ColorUtils.usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime);
                switch (true) {
                    case statsColor == "red" || expColor == "red":
                        return "#cf3c3c"; // Red
                    case statsColor == "orange" || expColor == "orange":
                        return "#f37b24"; // Orange
                    case statsColor == "green" || expColor == "green":
                        return "#008771"; // Green
                    default:
                        return "#7a316f"; // purple
                }
            },
            isClientEnabled(dbInbound, email) {
                clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null;
                return clientStats ? clientStats['enable'] : true;
            },
            isClientOnline(email) {
                return this.onlineClients.includes(email);
            },
            getLastOnline(email) {
                return this.lastOnlineMap[email] || null
            },
            formatLastOnline(email) {
                const ts = this.getLastOnline(email)
                if (!ts) return '-'
                if (this.datepicker === 'gregorian') {
                    return DateUtil.formatMillis(ts)
                }
                return DateUtil.convertToJalalian(moment(ts))
            },
            isRemovable(dbInboundId) {
                return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
            },
            inboundLinks(dbInboundId) {
                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
                newDbInbound = this.checkFallback(dbInbound);
                txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks(this.remarkModel), newDbInbound.remark);
            },
            exportSubs(dbInboundId) {
                const dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
                const clients = this.getInboundClients(dbInbound);
                let subLinks = []
                if (clients != null){
                    clients.forEach(c => {
                        if (c.subId && c.subId.length>0){
                            subLinks.push(this.subSettings.subURI + c.subId)
                        }
                    })
                }
                txtModal.show(
                    '{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}',
                    [...new Set(subLinks)].join('\n'),
                    dbInbound.remark + "-Subs");
            },
            importInbound() {
                promptModal.open({
                    title: '{{ i18n "pages.inbounds.importInbound" }}',
                    type: 'textarea',
                    value: '',
                    okText: '{{ i18n "pages.inbounds.import" }}',
                    confirm: async (dbInboundText) => {
                        await this.submit('/panel/api/inbounds/import', {data: dbInboundText}, promptModal);
                    },
                });
            },
            exportAllSubs() {
                let subLinks = []
                for (const dbInbound of this.dbInbounds) {
                    const clients = this.getInboundClients(dbInbound);
                    if (clients != null){
                        clients.forEach(c => {
                            if (c.subId && c.subId.length>0){
                                subLinks.push(this.subSettings.subURI + c.subId)
                            }
                        })
                    }
                }
                txtModal.show(
                    '{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}',
                    [...new Set(subLinks)].join('\r\n'),
                    'All-Inbounds-Subs');
            },
            exportAllLinks() {
                let copyText = [];
                for (const dbInbound of this.dbInbounds) {
                    copyText.push(dbInbound.genInboundLinks(this.remarkModel));
                }
                txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText.join('\r\n'), 'All-Inbounds');
            },
            copy(dbInboundId) {
              dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
              txtModal.show('{{ i18n "pages.inbounds.inboundData" }}', JSON.stringify(dbInbound, null, 2));
            },
            async startDataRefreshLoop() {
                while (this.isRefreshEnabled) {
                    try {
                        await this.getDBInbounds();
                    } catch (e) {
                        console.error(e);
                    }
                    await PromiseUtil.sleep(this.refreshInterval);
                }
            },
            toggleRefresh() {
                localStorage.setItem("isRefreshEnabled", this.isRefreshEnabled);
                if (this.isRefreshEnabled) {
                    this.startDataRefreshLoop();
                }
            },
            changeRefreshInterval() {
                localStorage.setItem("refreshInterval", this.refreshInterval);
            },
            async manualRefresh() {
                if (!this.refreshing) {
                    this.loadingStates.spinning = true;
                    await this.getDBInbounds();
                    this.loadingStates.spinning = false;
                }
            },
            pagination(obj){
                if (this.pageSize > 0 && obj.length>this.pageSize) {
                    // Set page options based on object size
                    sizeOptions = [];
                    for (i=this.pageSize;i<=obj.length;i=i+this.pageSize) {
                        sizeOptions.push(i.toString());
                    }
                    // Add option to see all in one page
                    sizeOptions.push(i.toString());

                    p = {
                        showSizeChanger: true,
                        size: 'small',
                        position: 'bottom',
                        pageSize: this.pageSize,
                        pageSizeOptions: sizeOptions
                    };
                    return p;
                }
                return false
            }
        },
        watch: {
            searchKey: Utils.debounce(function (newVal) {
                this.searchInbounds(newVal);
            }, 500)
        },
        computed: {
            total() {
                let down = 0, up = 0, allTime = 0;
                let clients = 0, deactive = [], depleted = [], expiring = [];
                this.dbInbounds.forEach(dbInbound => {
                    down += dbInbound.down;
                    up += dbInbound.up;
                    allTime += (dbInbound.allTime || (dbInbound.up + dbInbound.down));
                    if (this.clientCount[dbInbound.id]) {
                        clients += this.clientCount[dbInbound.id].clients;
                        deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
                        depleted = depleted.concat(this.clientCount[dbInbound.id].depleted);
                        expiring = expiring.concat(this.clientCount[dbInbound.id].expiring);
                    }
                });
                return {
                    down: down,
                    up: up,
                    allTime: allTime,
                    clients: clients,
                    deactive: deactive,
                    depleted: depleted,
                    expiring: expiring,
                };
            }
        },
    });
</script>
{{ template "page/body_end" .}}
